Coverage Report

Created: 2026-08-07 16:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\cssh-rs\cssh-rs\cssh-rs-core\src\cli.rs
Line
Count
Source
1
//! CLI interface
2
3
use crate::client::main as client_main;
4
use crate::daemon::{main as daemon_main, resolve_cluster_tags};
5
use crate::utils::config::{ClientConfig, Cluster, Config, ConfigOpt, DaemonConfig};
6
use crate::utils::windows::WindowsApi;
7
use crate::{
8
    current_exe_path, get_console_window_handle, init_logger, is_launched_from_gui,
9
    spawn_console_process, WindowsSettingsDefaultTerminalApplicationGuard,
10
};
11
use clap::{ArgAction, CommandFactory, Parser, Subcommand};
12
13
#[cfg(test)]
14
use mockall::{automock, predicate::*};
15
use windows::Win32::UI::HiDpi::PROCESS_PER_MONITOR_DPI_AWARE;
16
17
use cssh_rs_meta::PACKAGE_NAME;
18
19
/// Cross-platform cluster SSH tool
20
///
21
/// The main CLI arguments
22
#[derive(Parser, Debug)]
23
#[clap(author, version, about, long_about = None)]
24
pub struct Args {
25
    /// Optional subcommand
26
    /// Usually not specified by the user
27
    #[clap(subcommand)]
28
    command: Option<Commands>,
29
    /// Optional username used to connect to the hosts
30
    #[clap(long, short = 'u')]
31
    username: Option<String>,
32
    /// Optional port used for all SSH connections
33
    #[clap(long, short = 'p')]
34
    port: Option<u16>,
35
    /// Hosts and/or cluster tag(s) to connect to
36
    ///
37
    /// Hosts or cluster tags might use brace expansion,
38
    /// but need to be properly quoted.
39
    ///
40
    /// E.g.: `cssh-rs.exe "host{1..3}" hostA`
41
    ///
42
    /// Hosts can include a username which will take precedence over the
43
    /// username given via the `-u` option and over any ssh config value.
44
    ///
45
    /// E.g.: `cssh-rs.exe -u user3 user1@host1 userA@hostA host3`
46
    ///
47
    /// Hosts can include a port number which will take precedence over the
48
    /// port given via the `-p` option.
49
    ///
50
    /// E.g.: `cssh-rs.exe -p 33 host1:11 host2:22 host3`
51
    ///
52
    /// If no hosts are provided and the application is launched in a new console window
53
    /// (e.g. by double clicking the executable in the File Explorer),
54
    /// it will launch in interactive mode.
55
    #[clap(required = false, global = true)]
56
    hosts: Vec<String>,
57
    /// Enable extensive logging
58
    #[clap(short, long, action=ArgAction::SetTrue)]
59
    debug: bool,
60
}
61
62
/// The ``command`` CLI subcommand
63
#[derive(Debug, Subcommand, PartialEq)]
64
enum Commands {
65
    /// Subcommand that will launch a single client window
66
    ///
67
    /// connecting to the given host with the given username.
68
    /// It will also try to read input from a daemon via the named pipe.
69
    Client {
70
        /// Host to connect to
71
        host: String,
72
    },
73
    /// Subcommand that will launch the daemon window.
74
    ///
75
    /// The daemon is responsible to launch the client windows,
76
    /// one for each given host.
77
    /// For each client a named pipe will be created and any keystrokes
78
    /// the daemon window receives are forwarded via the pipes to all the clients.
79
    /// Also handles control mode.
80
    Daemon {},
81
    /// Write a default config file at the given output path.
82
    ///
83
    /// Values that differ from the defaults can be set via the options.
84
    GenerateConfig {
85
        /// Path to an SSH config file. When set, the launched program
86
        /// receives `-F <PATH>` as the first two argv entries.
87
        #[clap(long)]
88
        ssh_config_path: Option<String>,
89
        /// Program launched to establish each SSH connection.
90
        /// Defaults to the `client.program` config default when unset.
91
        #[clap(long)]
92
        program: Option<String>,
93
        /// Program arguments written to `client.arguments`, replacing the
94
        /// default bare `<user>@<host>` placeholder; repeatable and must
95
        /// include it. Use the `--arguments=VALUE` form for values starting
96
        /// with `-`, e.g. `--arguments=-XY --arguments={{USERNAME_AT_HOST}}`.
97
        #[clap(long)]
98
        arguments: Vec<String>,
99
        /// Name of the single cluster written to the config.
100
        #[clap(long, default_value = "default")]
101
        cluster: String,
102
        /// Path of the config file to write. Defaults to
103
        /// `cssh-rs-config.toml` next to the running executable.
104
        #[clap(long)]
105
        output: Option<String>,
106
    },
107
}
108
109
/// Main Entrypoint struct
110
///
111
/// Used to implement the entrypoint functions of the different
112
/// subcommands
113
pub struct MainEntrypoint;
114
115
/// Trait for Args operations to enable mocking in tests
116
#[cfg_attr(test, automock)]
117
pub trait ArgsCommand {
118
    /// Print help message
119
    fn print_help(&self) -> Result<(), std::io::Error>;
120
}
121
122
/// Default implementation of ArgsCommand trait
123
pub struct CLIArgsCommand;
124
125
impl ArgsCommand for CLIArgsCommand {
126
0
    fn print_help(&self) -> Result<(), std::io::Error> {
127
0
        return Args::command().print_help();
128
0
    }
129
}
130
131
/// Trait for logger initialization to enable mocking in tests
132
#[cfg_attr(test, automock)]
133
pub trait LoggerInitializer {
134
    /// Initialize logger with the given name
135
    fn init_logger(&self, name: &str);
136
}
137
138
/// Default implementation of LoggerInitializer trait
139
pub struct CLILoggerInitializer;
140
141
impl LoggerInitializer for CLILoggerInitializer {
142
0
    fn init_logger(&self, name: &str) {
143
0
        init_logger(name);
144
0
    }
145
}
146
147
/// Trait for writing output to enable dependency injection and testing
148
#[cfg_attr(test, automock)]
149
pub trait Output {
150
    /// Write a line to the output
151
    fn println(&mut self, text: &str);
152
    /// Write text without a newline to the output
153
    fn print(&mut self, text: &str);
154
    /// Write a line to stderr
155
    fn eprintln(&mut self, text: &str);
156
    /// Flush the output
157
    fn flush(&mut self);
158
}
159
160
/// Default implementation of Output trait that writes to stdout/stderr
161
pub struct CLIOutput;
162
163
impl Output for CLIOutput {
164
0
    fn println(&mut self, text: &str) {
165
0
        println!("{text}");
166
0
    }
167
168
0
    fn print(&mut self, text: &str) {
169
0
        print!("{text}");
170
0
    }
171
172
0
    fn eprintln(&mut self, text: &str) {
173
0
        eprintln!("{text}");
174
0
    }
175
176
0
    fn flush(&mut self) {
177
        use std::io::Write;
178
0
        std::io::stdout().flush().unwrap();
179
0
    }
180
}
181
182
/// Trait for reading input to enable dependency injection and testing
183
#[cfg_attr(test, automock)]
184
pub trait Input {
185
    /// Read a line from stdin
186
    fn read_line(&mut self) -> Result<String, std::io::Error>;
187
}
188
189
/// Default implementation of Input trait that reads from stdin
190
pub struct CLIInput;
191
192
impl Input for CLIInput {
193
0
    fn read_line(&mut self) -> Result<String, std::io::Error> {
194
0
        let mut input = String::new();
195
0
        std::io::stdin().read_line(&mut input)?;
196
0
        return Ok(input);
197
0
    }
198
}
199
200
/// Trait for environment operations to enable dependency injection and testing
201
#[cfg_attr(test, automock)]
202
pub trait Environment {
203
    /// Get current executable path
204
    fn current_exe(&self) -> Result<std::path::PathBuf, std::io::Error>;
205
    /// Set current directory
206
    fn set_current_dir(&self, path: &std::path::Path) -> Result<(), std::io::Error>;
207
}
208
209
/// Default implementation of Environment trait
210
pub struct CLIEnvironment;
211
212
impl Environment for CLIEnvironment {
213
0
    fn current_exe(&self) -> Result<std::path::PathBuf, std::io::Error> {
214
0
        return std::env::current_exe();
215
0
    }
216
217
0
    fn set_current_dir(&self, path: &std::path::Path) -> Result<(), std::io::Error> {
218
0
        return std::env::set_current_dir(path);
219
0
    }
220
}
221
222
/// Trait for configuration management to enable dependency injection and testing
223
#[cfg_attr(test, automock)]
224
pub trait ConfigManager {
225
    /// Load configuration from the specified path
226
    fn load_config(&self, path: &str) -> Result<ConfigOpt, confy::ConfyError>;
227
    /// Store configuration to the specified path
228
    fn store_config(&self, path: &str, config: &Config) -> Result<(), confy::ConfyError>;
229
}
230
231
/// Default implementation of ConfigManager trait
232
pub struct CLIConfigManager;
233
234
impl ConfigManager for CLIConfigManager {
235
0
    fn load_config(&self, path: &str) -> Result<ConfigOpt, confy::ConfyError> {
236
0
        return confy::load_path(path);
237
0
    }
238
239
0
    fn store_config(&self, path: &str, config: &Config) -> Result<(), confy::ConfyError> {
240
0
        return confy::store_path(path, config);
241
0
    }
242
}
243
244
/// Trait defining the entrypoint functions of the different
245
/// subcommands
246
#[cfg_attr(test, automock)]
247
pub trait Entrypoint {
248
    /// Entrypoint for the client subcommand
249
    fn client_main<W: WindowsApi + 'static>(
250
        &mut self,
251
        windows_api: &W,
252
        host: String,
253
        username: Option<String>,
254
        port: Option<u16>,
255
        config: &ClientConfig,
256
    ) -> impl std::future::Future<Output = ()> + Send;
257
    /// Entrypoint for the daemon subcommand
258
    fn daemon_main<W: WindowsApi + Clone + 'static>(
259
        &mut self,
260
        windows_api: &W,
261
        hosts: Vec<String>,
262
        username: Option<String>,
263
        port: Option<u16>,
264
        config: &DaemonConfig,
265
        clusters: &[Cluster],
266
        debug: bool,
267
    ) -> impl std::future::Future<Output = ()> + Send;
268
    /// Entrypoint for the main command
269
    fn main<W: WindowsApi + 'static, C: ConfigManager + 'static>(
270
        &mut self,
271
        windows_api: &W,
272
        config_manager: &C,
273
        config_path: &str,
274
        config: &Config,
275
        args: Args,
276
    );
277
}
278
279
impl Entrypoint for MainEntrypoint {
280
0
    async fn client_main<W: WindowsApi>(
281
0
        &mut self,
282
0
        windows_api: &W,
283
0
        host: String,
284
0
        username: Option<String>,
285
0
        port: Option<u16>,
286
0
        config: &ClientConfig,
287
0
    ) {
288
0
        client_main(windows_api, host, username, port, config).await;
289
0
    }
290
291
0
    async fn daemon_main<W: WindowsApi + Clone + 'static>(
292
0
        &mut self,
293
0
        windows_api: &W,
294
0
        hosts: Vec<String>,
295
0
        username: Option<String>,
296
0
        port: Option<u16>,
297
0
        config: &DaemonConfig,
298
0
        clusters: &[Cluster],
299
0
        debug: bool,
300
0
    ) {
301
0
        daemon_main(windows_api, hosts, username, port, config, clusters, debug).await;
302
0
    }
303
304
7
    fn main<W: WindowsApi + 'static, C: ConfigManager + 'static>(
305
7
        &mut self,
306
7
        windows_api: &W,
307
7
        config_manager: &C,
308
7
        config_path: &str,
309
7
        config: &Config,
310
7
        args: Args,
311
7
    ) {
312
7
        config_manager.store_config(config_path, config).unwrap();
313
314
7
        let mut daemon_args: Vec<String> = Vec::new();
315
7
        if args.debug {
316
2
            daemon_args.push("-d".to_string());
317
5
        }
318
7
        if let Some(
username3
) = args.username {
319
3
            daemon_args.push("-u".to_string());
320
3
            daemon_args.push(username);
321
4
        }
322
7
        if let Some(
port3
) = args.port {
323
3
            daemon_args.push("-p".to_string());
324
3
            daemon_args.push(port.to_string());
325
4
        }
326
7
        daemon_args.push("daemon".to_string());
327
        // Order is important here. If the hosts are passed before the daemon subcommand
328
        // it will not be recognizes as such and just be passed along as one of the hosts.
329
7
        daemon_args.extend(
330
7
            resolve_cluster_tags(
331
9
                
args.hosts.iter()7
.
map7
(|host| return &**host).
collect7
(),
332
7
                &config.clusters,
333
            )
334
7
            .into_iter()
335
9
            .
map7
(|host| return host.to_string()),
336
        );
337
7
        let _guard = WindowsSettingsDefaultTerminalApplicationGuard::new();
338
        // We must wait for the window to actually launch before dropping the _guard as we might otherwise
339
        // reset the configuration before the window was launched
340
7
        let _ = get_console_window_handle(
341
7
            windows_api,
342
7
            spawn_console_process(windows_api, &current_exe_path(), daemon_args, true)
343
7
                .expect("Failed to create process")
344
7
                .dwProcessId,
345
7
        );
346
7
    }
347
}
348
349
/// Display the interactive mode prompt and instructions
350
12
fn show_interactive_prompt<O: Output>(output: &mut O) {
351
12
    output.println("\n=== Interactive Mode ===");
352
12
    output.println(&format!(
353
12
        "Enter your {PACKAGE_NAME} arguments (or press Enter to exit):"
354
12
    ));
355
12
    output.println("Example: -u myuser host1 host2 host3");
356
12
    output.println("Example: --help");
357
12
    output.print("> ");
358
12
    output.flush();
359
12
}
360
361
/// Read user input from stdin
362
///
363
/// # Arguments
364
///
365
/// * `input` - The Input trait object for reading from stdin
366
///
367
/// # Returns
368
///
369
/// * `Ok(Some(input))` - User provided input
370
/// * `Ok(None)` - User wants to exit (empty input or "exit")
371
/// * `Err(error)` - Error reading input
372
16
fn read_user_input<I: Input>(input: &mut I) -> Result<Option<String>, std::io::Error> {
373
16
    let 
input_line14
= input.read_line()
?2
;
374
375
14
    let input_trimmed = input_line.trim();
376
14
    if input_trimmed.is_empty() || 
input_trimmed.to_lowercase() == "exit"7
{
377
9
        return Ok(None);
378
5
    }
379
380
5
    return Ok(Some(input_trimmed.to_string()));
381
16
}
382
383
/// Handle special commands that don't need full parsing
384
///
385
/// # Arguments
386
///
387
/// * `input` - The user input string
388
/// * `args_command` - The ArgsCommand trait object for printing help
389
///
390
/// # Returns
391
///
392
/// * `true` - Command was handled, continue loop
393
/// * `false` - Command needs full parsing
394
13
fn handle_special_commands<A: ArgsCommand>(input: &str, args_command: &A) -> bool {
395
13
    if input == "--help" || 
input == "-h"11
{
396
3
        let _ = args_command.print_help();
397
3
        return true;
398
10
    }
399
10
    return false;
400
13
}
401
402
/// Execute the interactively entered command, rejecting any subcommand.
403
///
404
/// Interactive mode accepts only cssh options and positional arguments.
405
6
async fn execute_parsed_command<
406
6
    W: WindowsApi + Clone + 'static,
407
6
    T: Entrypoint,
408
6
    A: ArgsCommand,
409
6
    O: Output,
410
6
    C: ConfigManager + 'static,
411
6
>(
412
6
    windows_api: &W,
413
6
    parsed_args: Args,
414
6
    entrypoint: &mut T,
415
6
    args_command: &A,
416
6
    output: &mut O,
417
6
    config_manager: &C,
418
6
    config: &Config,
419
6
    config_path: &str,
420
6
) {
421
6
    match &parsed_args.command {
422
3
        Some(_) => {
423
3
            output.eprintln(
424
3
                "Subcommands are not supported in interactive mode. Use cssh options and positional arguments only.",
425
3
            );
426
3
        }
427
        None => {
428
3
            if !parsed_args.hosts.is_empty() {
429
2
                entrypoint.main(
430
2
                    windows_api,
431
2
                    config_manager,
432
2
                    config_path,
433
2
                    config,
434
2
                    parsed_args,
435
2
                );
436
2
            } else {
437
1
                // Show help for empty hosts
438
1
                let _ = args_command.print_help();
439
1
            }
440
        }
441
    }
442
6
}
443
444
/// Run the interactive mode loop for GUI launches
445
6
async fn run_interactive_mode<
446
6
    W: WindowsApi + Clone + 'static,
447
6
    A: ArgsCommand,
448
6
    T: Entrypoint,
449
6
    O: Output,
450
6
    I: Input,
451
6
    C: ConfigManager + 'static,
452
6
>(
453
6
    windows_api: &W,
454
6
    args_command: &A,
455
6
    mut entrypoint: T,
456
6
    config_manager: &C,
457
6
    config: &Config,
458
6
    config_path: &str,
459
6
    output: &mut O,
460
6
    input: &mut I,
461
6
) {
462
    loop {
463
11
        show_interactive_prompt(output);
464
465
11
        match read_user_input(input) {
466
4
            Ok(Some(input_str)) => {
467
                // Handle special commands first
468
4
                if handle_special_commands(&input_str, args_command) {
469
1
                    continue;
470
3
                }
471
472
                // Parse the input as command line arguments
473
3
                let input_args: Vec<&str> = input_str.split_whitespace().collect();
474
3
                let mut full_args = vec![PACKAGE_NAME];
475
3
                full_args.extend(input_args);
476
477
3
                match Args::try_parse_from(full_args) {
478
2
                    Ok(parsed_args) => {
479
2
                        execute_parsed_command(
480
2
                            windows_api,
481
2
                            parsed_args,
482
2
                            &mut entrypoint,
483
2
                            args_command,
484
2
                            output,
485
2
                            config_manager,
486
2
                            config,
487
2
                            config_path,
488
2
                        )
489
2
                        .await;
490
                    }
491
1
                    Err(err) => {
492
1
                        output.eprintln(&format!("\nError parsing arguments: {err}"));
493
1
                    }
494
                }
495
            }
496
            Ok(None) => {
497
6
                return;
498
            }
499
1
            Err(err) => {
500
1
                output.eprintln(&format!("Error reading input: {err}"));
501
1
            }
502
        }
503
    }
504
6
}
505
506
/// Build the config emitted by `generate-config`. A set `ssh_config_path` both
507
/// prepends `-F <path>` to `client.arguments` and sets `client.ssh_config_path`;
508
/// non-empty `arguments` replace the default `<user>@<host>` placeholder.
509
6
fn build_generate_config(
510
6
    hosts: Vec<String>,
511
6
    cluster: &str,
512
6
    program: Option<&str>,
513
6
    ssh_config_path: Option<&str>,
514
6
    arguments: Vec<String>,
515
6
) -> Config {
516
6
    let mut config = Config {
517
6
        clusters: vec![Cluster {
518
6
            name: cluster.to_string(),
519
6
            hosts,
520
6
        }],
521
6
        ..Config::default()
522
6
    };
523
524
6
    if let Some(
program5
) = program {
525
5
        config.client.program = program.to_string();
526
5
    
}1
527
528
6
    let mut client_arguments = Vec::new();
529
6
    if let Some(
path3
) = ssh_config_path {
530
3
        client_arguments.push("-F".to_string());
531
3
        client_arguments.push(path.to_string());
532
3
        config.client.ssh_config_path = path.to_string();
533
3
    }
534
6
    if arguments.is_empty() {
535
5
        client_arguments.push(config.client.username_host_placeholder.clone());
536
5
    } else {
537
1
        client_arguments.extend(arguments);
538
1
    }
539
6
    config.client.arguments = client_arguments;
540
541
6
    return config;
542
6
}
543
544
/// Write the generated config to `output_path` (or `default_config_path`) and
545
/// print its absolute path to `output`; error if `hosts` is empty or the write
546
/// fails.
547
3
fn run_generate_config<O: Output, C: ConfigManager>(
548
3
    output: &mut O,
549
3
    config_manager: &C,
550
3
    default_config_path: &str,
551
3
    hosts: Vec<String>,
552
3
    cluster: &str,
553
3
    program: Option<&str>,
554
3
    ssh_config_path: Option<&str>,
555
3
    arguments: Vec<String>,
556
3
    output_path: Option<&str>,
557
3
) -> Result<(), String> {
558
3
    if hosts.is_empty() {
559
1
        return Err("generate-config requires at least one host".to_string());
560
2
    }
561
562
2
    let config = build_generate_config(hosts, cluster, program, ssh_config_path, arguments);
563
2
    let target_path = std::path::PathBuf::from(output_path.unwrap_or(default_config_path));
564
2
    let target_str = target_path.to_string_lossy().into_owned();
565
566
2
    config_manager
567
2
        .store_config(&target_str, &config)
568
2
        .map_err(|err| return 
format!0
("Failed to write config to {target_str}: {err}"))
?0
;
569
570
    // canonicalize can fail on some filesystems; fall back to a lexical absolute path.
571
2
    let resolved = std::fs::canonicalize(&target_path)
572
2
        .or_else(|_| return std::path::absolute(&target_path))
573
2
        .map(|p| return p.to_string_lossy().into_owned())
574
2
        .unwrap_or(target_str);
575
2
    output.println(&resolved);
576
2
    return Ok(());
577
3
}
578
579
/// The main entrypoint
580
///
581
/// Parses the CLI arguments,
582
/// loads an existing config or writes the default config to disk, and
583
/// calls the respective subcommand.
584
/// If no subcommand is given we launch the daemon subcommand in a new window.
585
10
pub async fn main<
586
10
    W: WindowsApi + Clone + 'static,
587
10
    E: Entrypoint,
588
10
    O: Output,
589
10
    I: Input,
590
10
    Env: Environment,
591
10
    A: ArgsCommand,
592
10
    L: LoggerInitializer,
593
10
    C: ConfigManager + 'static,
594
10
>(
595
10
    windows_api: &W,
596
10
    args: Args,
597
10
    mut entrypoint: E,
598
10
    output: &mut O,
599
10
    input: &mut I,
600
10
    environment: &Env,
601
10
    args_command: &A,
602
10
    logger_initializer: &L,
603
10
    config_manager: &C,
604
10
) {
605
    // CRITICAL: Check GUI launch BEFORE any output to console
606
10
    let launched_from_gui = is_launched_from_gui(windows_api);
607
608
    // Set DPI awareness programatically. Using the manifest is the recommended way
609
    // but conhost.exe does not do any manifest loading.
610
    // https://github.com/microsoft/terminal/issues/18464#issuecomment-2623392013
611
10
    if let Err(
err4
) = windows_api.set_process_dpi_awareness(PROCESS_PER_MONITOR_DPI_AWARE) {
612
4
        output.eprintln(&format!(
613
4
            "Failed to set DPI awareness programatically: {err:?}"
614
4
        ));
615
6
    }
616
10
    match environment.current_exe() {
617
9
        Ok(path) => match path.parent() {
618
1
            None => {
619
1
                output.eprintln("Failed to get executable path parent working directory");
620
1
            }
621
8
            Some(exe_dir) => {
622
8
                environment
623
8
                    .set_current_dir(exe_dir)
624
8
                    .expect("Failed to change current working directory");
625
8
            }
626
        },
627
1
        Err(_) => {
628
1
            output.eprintln("Failed to get executable directory");
629
1
        }
630
    }
631
632
10
    let config_path = format!("{PACKAGE_NAME}-config.toml");
633
634
    // Dispatch before load_config so a broken on-disk config cannot break generation.
635
    if let Some(Commands::GenerateConfig {
636
0
        ssh_config_path,
637
0
        program,
638
0
        arguments,
639
0
        cluster,
640
0
        output: output_path,
641
4
    }) = &args.command
642
    {
643
0
        if let Err(err) = run_generate_config(
644
0
            output,
645
0
            config_manager,
646
0
            &config_path,
647
0
            args.hosts.to_owned(),
648
0
            cluster,
649
0
            program.as_deref(),
650
0
            ssh_config_path.as_deref(),
651
0
            arguments.to_owned(),
652
0
            output_path.as_deref(),
653
0
        ) {
654
0
            output.eprintln(&err);
655
0
            std::process::exit(1);
656
0
        }
657
0
        return;
658
10
    }
659
660
10
    let config_on_disk: ConfigOpt = config_manager.load_config(&config_path).unwrap();
661
10
    let config: Config = config_on_disk.into();
662
663
4
    match &args.command {
664
0
        Some(Commands::GenerateConfig { .. }) => unreachable!("handled before config load"),
665
2
        Some(Commands::Client { host }) => {
666
2
            if args.debug {
667
1
                logger_initializer.init_logger(&format!("cssh-rs_client_{host}"));
668
1
            }
669
2
            entrypoint
670
2
                .client_main(
671
2
                    windows_api,
672
2
                    host.to_owned(),
673
2
                    args.username.to_owned(),
674
2
                    args.port,
675
2
                    &config.client,
676
2
                )
677
2
                .await;
678
        }
679
        Some(Commands::Daemon {}) => {
680
2
            if args.debug {
681
1
                logger_initializer.init_logger("cssh-rs_daemon");
682
1
            }
683
2
            entrypoint
684
2
                .daemon_main(
685
2
                    windows_api,
686
2
                    args.hosts.to_owned(),
687
2
                    args.username.clone(),
688
2
                    args.port,
689
2
                    &config.daemon,
690
2
                    &config.clusters,
691
2
                    args.debug,
692
2
                )
693
2
                .await;
694
        }
695
        None => {
696
            // If no hosts provided, show help and handle GUI vs console launch
697
6
            if args.hosts.is_empty() {
698
5
                let _ = args_command.print_help();
699
700
                // If launched from GUI, allow user to input arguments interactively
701
5
                if launched_from_gui {
702
2
                    run_interactive_mode(
703
2
                        windows_api,
704
2
                        args_command,
705
2
                        entrypoint,
706
2
                        config_manager,
707
2
                        &config,
708
2
                        &config_path,
709
2
                        output,
710
2
                        input,
711
2
                    )
712
2
                    .await;
713
3
                }
714
5
                return;
715
1
            }
716
717
1
            entrypoint.main(windows_api, config_manager, &config_path, &config, args);
718
        }
719
    }
720
10
}
721
722
#[cfg(test)]
723
#[path = "./tests/test_cli.rs"]
724
mod test_cli;